package com.nageoffer.onecoupon.engine.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.map.MapUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nageoffer.onecoupon.engine.common.constant.EngineRedisConstant;
import com.nageoffer.onecoupon.engine.common.enums.CouponTemplateStatusEnum;
import com.nageoffer.onecoupon.engine.dao.entity.CouponTemplateDO;
import com.nageoffer.onecoupon.engine.dao.mapper.CouponTemplateMapper;
import com.nageoffer.onecoupon.engine.dto.req.CouponTemplateQueryReqDTO;
import com.nageoffer.onecoupon.engine.dto.resp.CouponTemplateQueryRespDTO;
import com.nageoffer.onecoupon.engine.service.CouponTemplateService;
import com.nageoffer.onecoupon.framework.exception.ClientException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static com.nageoffer.onecoupon.engine.common.constant.EngineRedisConstant.COUPON_TEMPLATE_KEY;
import static com.nageoffer.onecoupon.engine.common.constant.EngineRedisConstant.LOCK_COUPON_TEMPLATE_KEY;

/**
 * 优惠券模板业务逻辑实现层
 * <p>
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class CouponTemplateServiceImpl extends ServiceImpl<CouponTemplateMapper, CouponTemplateDO> implements CouponTemplateService {

    private final CouponTemplateMapper couponTemplateMapper;
    private final StringRedisTemplate redisTemplate;
    private final RedissonClient redissonClient;
    private final RBloomFilter<String> couponTemplateQueryBloomFilter;

    @Override
    public CouponTemplateQueryRespDTO findCouponTemplate(CouponTemplateQueryReqDTO requestParam) {
        LambdaQueryWrapper<CouponTemplateDO> queryWrapper = Wrappers.lambdaQuery(CouponTemplateDO.class)
                .eq(CouponTemplateDO::getShopNumber, Long.parseLong(requestParam.getShopNumber()))
                .eq(CouponTemplateDO::getId, Long.parseLong(requestParam.getCouponTemplateId()))
                .eq(CouponTemplateDO::getStatus, CouponTemplateStatusEnum.ACTIVE.getStatus());
        CouponTemplateDO couponTemplateDO = couponTemplateMapper.selectOne(queryWrapper);
        if (couponTemplateDO == null) {
            throw new ClientException("优惠券模板不存在或已过期");
        }
        return BeanUtil.toBean(couponTemplateDO, CouponTemplateQueryRespDTO.class);
    }

    @Override
    public CouponTemplateQueryRespDTO findCouponTemplateV2(CouponTemplateQueryReqDTO requestParam) {
        String templateId = requestParam.getCouponTemplateId();
        // 查询redis中是否存在优惠券模板信息
        String templateCacheKey = String.format(COUPON_TEMPLATE_KEY, templateId);
        Map<Object, Object> templateCacheMap = redisTemplate.opsForHash().entries(templateCacheKey);
        // 缓存中存在则直接返回，不存在则使用双重判定锁查数据库
        if (MapUtil.isEmpty(templateCacheMap)) {
            // 获取模板缓存分布式锁
            RLock lock = redissonClient.getLock(String.format(LOCK_COUPON_TEMPLATE_KEY, templateId));
            lock.lock();
            try {
                // 双重判定锁
                templateCacheMap = redisTemplate.opsForHash().entries(templateCacheKey);
                if (MapUtil.isEmpty(templateCacheMap)) {
                    templateCacheMap = loadTemplateCache(requestParam);
                }
            } finally {
                lock.unlock();
            }
        }
        return BeanUtil.mapToBean(templateCacheMap, CouponTemplateQueryRespDTO.class, false, CopyOptions.create());
    }

    @Override
    public CouponTemplateQueryRespDTO findCouponTemplateV3(CouponTemplateQueryReqDTO requestParam) {
        String templateId = requestParam.getCouponTemplateId();
        // 查询redis中是否存在优惠券模板信息
        String templateCacheKey = String.format(COUPON_TEMPLATE_KEY, templateId);
        Map<Object, Object> templateCacheMap = redisTemplate.opsForHash().entries(templateCacheKey);
        // 缓存中不存在
        if (MapUtil.isEmpty(templateCacheMap)) {
            // 查询布隆过滤器中是否存在：如果存在可能误判，如果不存在则一定不存在
            if (!couponTemplateQueryBloomFilter.contains(templateId)) {
                log.error("{}不存在布隆过滤器中", templateId);
                throw new ClientException("优惠券模板不存在");
            }
            // 查询当前模板id是否在缓存中为空值：存在则直接返回
            String couponTemplateIsNullCacheKey = String.format(EngineRedisConstant.COUPON_TEMPLATE_IS_NULL_KEY, requestParam.getCouponTemplateId());
            Boolean hasKeyFlag = redisTemplate.hasKey(couponTemplateIsNullCacheKey);
            if (Boolean.TRUE.equals(hasKeyFlag)) {
                log.error("{}存在空值缓存", templateId);
                throw new ClientException("优惠券模板不存在");
            }
            // 获取优惠券模板分布式锁
            RLock lock = redissonClient.getLock(String.format(EngineRedisConstant.LOCK_COUPON_TEMPLATE_KEY, requestParam.getCouponTemplateId()));
            lock.lock();
            try {
                // 双重判定锁
                templateCacheMap = redisTemplate.opsForHash().entries(templateCacheKey);
                if (MapUtil.isEmpty(templateCacheMap)) {
                    // 查询数据库并加载到缓存中
                    templateCacheMap = loadTemplateCache(requestParam);
                }
            } finally {
                lock.unlock();
            }
        }
        return BeanUtil.mapToBean(templateCacheMap, CouponTemplateQueryRespDTO.class, false, CopyOptions.create());
    }

    /**
     * 加载优惠券模板到缓存中
     *
     * @param requestParam 请求参数
     */
    private Map<Object, Object> loadTemplateCache(CouponTemplateQueryReqDTO requestParam) {
        String templateId = requestParam.getCouponTemplateId();
        // 查询redis中是否存在优惠券模板信息
        String templateCacheKey = String.format(COUPON_TEMPLATE_KEY, templateId);
        Map<Object, Object> templateCacheMap;
        LambdaQueryWrapper<CouponTemplateDO> queryWrapper = Wrappers.lambdaQuery(CouponTemplateDO.class)
                .eq(CouponTemplateDO::getShopNumber, Long.parseLong(requestParam.getShopNumber()))
                .eq(CouponTemplateDO::getId, Long.parseLong(requestParam.getCouponTemplateId()))
                .eq(CouponTemplateDO::getStatus, CouponTemplateStatusEnum.ACTIVE.getStatus());
        CouponTemplateDO couponTemplateDO = couponTemplateMapper.selectOne(queryWrapper);
        // 优惠券模板不存在或者已过期加入空值缓存，并且抛出异常
        String couponTemplateIsNullCacheKey = String.format(EngineRedisConstant.COUPON_TEMPLATE_IS_NULL_KEY, requestParam.getCouponTemplateId());
        if (couponTemplateDO == null) {
            redisTemplate.opsForValue().set(couponTemplateIsNullCacheKey, "", 30, TimeUnit.MINUTES);
            throw new ClientException("优惠券模板不存在或已过期");
        }
        // 通过将数据库的记录序列化成JSON字符串放入缓存
        CouponTemplateQueryRespDTO actualRespDTO = BeanUtil.toBean(couponTemplateDO, CouponTemplateQueryRespDTO.class);
        Map<String, Object> cacheTargetMap = BeanUtil.beanToMap(actualRespDTO, false, true);
        Map<String, String> actualCacheTargetMap = cacheTargetMap.entrySet().stream()
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        entry -> entry.getValue() != null ? entry.getValue().toString() : ""
                ));
        long timeout = couponTemplateDO.getValidEndTime().getTime() / 1000;
        redisTemplate.opsForHash().putAll(templateCacheKey, actualCacheTargetMap);
        redisTemplate.expire(templateCacheKey, timeout, TimeUnit.SECONDS);
        //loadCacheByLua(templateCacheKey, timeout, actualCacheTargetMap);
        log.info("{}加载到缓存中完成过期时间为{}", templateCacheKey, timeout);
        templateCacheMap = cacheTargetMap.entrySet()
                .stream()
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
        return templateCacheMap;
    }

    /**
     * 使用lua脚本加载进缓存中：保证数据和超时时间加载的原子性
     *
     * @param templateCacheKey     缓存key
     * @param timeout              超时时间
     * @param actualCacheTargetMap 缓存的map对象
     */
    private void loadCacheByLua(String templateCacheKey, Long timeout, Map<String, String> actualCacheTargetMap) {
        // 通过LUA脚本执行设置Hash数据以及设置过期时间
        String luaScript = "redis.call('HMSET', KEYS[1], unpack(ARGV, 1, #ARGV - 1)) " +
                "redis.call('EXPIREAT', KEYS[1], ARGV[#ARGV])";
        List<String> keys = Collections.singletonList(templateCacheKey);
        List<String> args = new ArrayList<>(actualCacheTargetMap.size() * 2 + 1);
        actualCacheTargetMap.forEach((key, value) -> {
            args.add(key);
            args.add(value);
        });
        // 优惠券活动过期时间转换为秒级别的 Unix 时间戳
        args.add(String.valueOf(timeout));
        // 执行 LUA 脚本
        redisTemplate.execute(
                new DefaultRedisScript<>(luaScript, Long.class),
                keys,
                args.toArray()
        );
    }


}
